﻿using System;
using System.Collections.Generic;
using System.ComponentModel;

namespace Notoric.Model.Collections
{
    /// <summary>
    /// Implements a simple wrapper around a collection that prevents modifications of the collection.
    /// The wrapped collection must derive from <see cref="ObservableCollection{T}"/>.
    /// </summary>
    /// <typeparam name="T">
    /// The type of the collection items.
    /// </typeparam>
    public class ReadOnlyCollection<T> : INotifyCollectionChanged, INotifyPropertyChanged, IEnumerable<T>, IList<T>
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="ReadOnlyCollection{T}"/> instance.
        /// </summary>
        /// <param name="collection">
        /// <para>
        /// Type: <see cref="ObservableCollection{T}"/>
        /// </para>
        /// <para>
        /// The wrapped collection.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="collection"/> is <see langword="null"/>.
        /// </exception>
        public ReadOnlyCollection(IList<T> collection)
        {
            if (collection == null)
                throw new ArgumentNullException("collection");

            this.collection = collection;
            if (collection is INotifyPropertyChanged)
                ((INotifyPropertyChanged)collection).PropertyChanged += new PropertyChangedEventHandler(collection_PropertyChanged);
            if (collection is INotifyCollectionChanged)
                ((INotifyCollectionChanged)collection).CollectionChanged += new EventHandler<NotifyCollectionChangedEventArgs>(collection_CollectionChanged);
        }

        #endregion

        #region INotifyCollectionChanged Members

        /// <inheritdoc/>
        public event EventHandler<NotifyCollectionChangedEventArgs> CollectionChanged;

        #endregion

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Occurs when the value of some property has changed.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region IEnumerable<T> Members

        /// <inheritdoc/>
        public IEnumerator<T> GetEnumerator()
        {
            return collection.GetEnumerator();
        }

        #endregion

        #region IEnumerable Members

        /// <inheritdoc/>
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return collection.GetEnumerator();
        }

        #endregion

        #region IList<T> Members

        /// <inheritdoc/>
        void IList<T>.Insert(int index, T item)
        {
            throw new NotSupportedException();
        }

        /// <inheritdoc/>
        void IList<T>.RemoveAt(int index)
        {
            throw new NotSupportedException();
        }

        /// <inheritdoc/>
        T IList<T>.this[int index]
        {
            get
            {
                return this[index];
            }
            set
            {
                throw new NotSupportedException();
            }
        }

        #endregion

        #region ICollection<T> Members

        /// <inheritdoc/>
        void ICollection<T>.Add(T item)
        {
            throw new NotSupportedException();
        }

        /// <inheritdoc/>
        void ICollection<T>.Clear()
        {
            throw new NotSupportedException();
        }

        /// <inheritdoc/>
        bool ICollection<T>.IsReadOnly
        {
            get { return true; }
        }

        /// <inheritdoc/>
        bool ICollection<T>.Remove(T item)
        {
            throw new NotSupportedException();
        }

        #endregion

        #region Event Handlers

        /// <summary>
        /// Occurs when the wrapped collection has changed.
        /// </summary>
        /// <remarks>
        /// <b>Operation:</b><br/>
        /// The handler just resends the event via the class's own <see cref="CollectionChanged"/> event.
        /// </remarks>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        void collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (CollectionChanged != null)
                CollectionChanged(this, e);
        }

        /// <summary>
        /// Occurs when the value of some property of the wrapped collection has changed.
        /// </summary>
        /// <remarks>
        /// <b>Operation:</b><br/>
        /// The handler just resends the event via the class's own <see cref="PropertyChanged"/> event.
        /// </remarks>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        void collection_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, e);
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Searches a range of elements in the sorted collection for an element using the specified comparer and returns the zero-based index of the element.
        /// </summary>
        /// <param name="index">
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based starting index of the range to search.
        /// </para>
        /// </param>
        /// <param name="count">
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The length of the range to search.
        /// </para>
        /// </param>
        /// <param name="item">
        /// <para>
        /// Type: <typeparamref name="T"/>
        /// </para>
        /// <para>
        /// The object to locate. The value can be <see langword="null"/> for reference types.
        /// </para>
        /// </param>
        /// <param name="comparer">
        /// <para>
        /// Type: <see cref="IComparer{T}"/> of <typeparamref name="T"/>
        /// </para>
        /// <para>
        /// The <see cref="IComparer{T}"/> implementation to use when comparing elements, or <see langword="null"/> to use the default comparer <see cref="Comparer{T}.Default"/>.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index of item in the sorted collection, if item is found; otherwise -1.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <paramref name="index"/> is less than 0.<br/>
        /// -or-<br/>
        /// <paramref name="count"/> is less than 0.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// <paramref name="index"/> and <paramref name="count"/> do not denote a valid range in the collection.
        /// </exception>
        /// <exception cref="InvalidOperationException">
        /// <paramref name="comparer"/> is <see langword="null"/> and the default comparer <see cref="Comparer{T}.Default"/> cannot
        /// find an implementation of the <see cref="IComparable{T}"/> generic interface or the <see cref="IComparable"/> interface for type <typeparamref name="T"/>.
        /// </exception>
        public int BinarySearch(int index, int count, T item, IComparer<T> comparer)
        {
            if (index < 0)
                throw new ArgumentOutOfRangeException("index", "Index is less than 0!");
            if (count < 0)
                throw new ArgumentOutOfRangeException("count", "Count is less than 0!");
            if (index + count > collection.Count)
                throw new ArgumentException("Index and count do not denote a valid range!");
            if (comparer == null)
            {
                try
                {
                    comparer = Comparer<T>.Default;
                }
                catch (Exception)
                {
                    throw new InvalidOperationException("Default comparer cannot find an implementation of IComparable<T> interface for the type " + typeof(T).FullName + "!");
                }
            }

            int l = index;
            int u = index + count - 1;

            while (l <= u)
            {
                int i = l + ((u - l) >> 1);
                int r = comparer.Compare(collection[i], item);

                if (r == 0)
                    return i;
                else if (r < 0)
                    l = i + 1;
                else
                    u = i - 1;
            }

            return -1;
        }

        /// <summary>
        /// Searches the entire sorted collection for an element using the specified comparer and returns the zero-based index of the element.
        /// </summary>
        /// <param name="item">
        /// <para>
        /// Type: <typeparamref name="T"/>
        /// </para>
        /// <para>
        /// The object to locate. The value can be <see langword="null"/> for reference types.
        /// </para>
        /// </param>
        /// <param name="comparer">
        /// <para>
        /// Type: <see cref="IComparer{T}"/> of <typeparamref name="T"/>
        /// </para>
        /// <para>
        /// The <see cref="IComparer{T}"/> implementation to use when comparing elements, or <see langword="null"/> to use the default comparer <see cref="Comparer{T}.Default"/>.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index of item in the sorted collection, if item is found; otherwise -1.
        /// </para>
        /// </returns>
        /// <exception cref="InvalidOperationException">
        /// <paramref name="comparer"/> is <see langword="null"/> and the default comparer <see cref="Comparer{T}.Default"/> cannot
        /// find an implementation of the <see cref="IComparable{T}"/> generic interface or the <see cref="IComparable"/> interface for type <typeparamref name="T"/>.
        /// </exception>
        public int BinarySearch(T item, IComparer<T> comparer)
        {
            return BinarySearch(0, collection.Count, item, comparer);
        }

        /// <summary>
        /// Searches the entire sorted collection for an element using the default comparer and returns the zero-based index of the element.
        /// </summary>
        /// <param name="item">
        /// <para>
        /// Type: <typeparamref name="T"/>
        /// </para>
        /// <para>
        /// The object to locate. The value can be <see langword="null"/> for reference types.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index of item in the sorted collection, if item is found; otherwise -1.
        /// </para>
        /// </returns>
        /// <exception cref="InvalidOperationException">
        /// The default comparer <see cref="Comparer{T}.Default"/> cannot find an implementation of the 
        /// <see cref="IComparable{T}"/> generic interface or the <see cref="IComparable"/> interface for type <typeparamref name="T"/>.
        /// </exception>
        public int BinarySearch(T item)
        {
            return BinarySearch(0, collection.Count, item, null);
        }

        /// <summary>
        /// Searches a range of elements in the sorted collection for an element using the specified compare method and returns the zero-based index of the element.
        /// </summary>
        /// <param name="index">
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based starting index of the range to search.
        /// </para>
        /// </param>
        /// <param name="count">
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The length of the range to search.
        /// </para>
        /// </param>
        /// <param name="predicate">
        /// <para>
        /// Type: <see cref="SortedSearchPredicate{T}"/> of <typeparamref name="T"/>
        /// </para>
        /// <para>
        /// The method that compares the items in the collection to the searched element.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index of item in the sorted collection, if item is found; otherwise -1.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <paramref name="index"/> is less than 0.<br/>
        /// -or-<br/>
        /// <paramref name="count"/> is less than 0.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// <paramref name="index"/> and <paramref name="count"/> do not denote a valid range in the collection.
        /// </exception>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="predicate"/> is <see langword="null"/>.
        /// </exception>
        public int BinarySearch(int index, int count, SortedSearchPredicate<T> predicate)
        {
            if (index < 0)
                throw new ArgumentOutOfRangeException("index", "Index is less than 0!");
            if (count < 0)
                throw new ArgumentOutOfRangeException("count", "Count is less than 0!");
            if (index + count > collection.Count)
                throw new ArgumentException("Index and count do not denote a valid range!");
            if (predicate == null)
                throw new ArgumentNullException("predicate");

            int l = index;
            int u = index + count - 1;

            while (l <= u)
            {
                int i = l + ((u - l) >> 1);
                int r = predicate(collection[i]);

                if (r == 0)
                    return i;
                else if (r < 0)
                    l = i + 1;
                else
                    u = i - 1;
            }

            return -1;
        }

        /// <summary>
        /// Searches the entire sorted collection for an element using the specified compare method and returns the zero-based index of the element.
        /// </summary>
        /// <param name="predicate">
        /// <para>
        /// Type: <see cref="SortedSearchPredicate{T}"/> of <typeparamref name="T"/>
        /// </para>
        /// <para>
        /// The method that compares the items in the collection to the searched element.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index of item in the sorted collection, if item is found; otherwise -1.
        /// </para>
        /// </returns>       
        /// <exception cref="ArgumentNullException">
        /// <paramref name="predicate"/> is <see langword="null"/>.
        /// </exception>
        public int BinarySearch(SortedSearchPredicate<T> predicate)
        {
            return BinarySearch(0, collection.Count, predicate);
        }

        /// <summary>
        /// Determines whether an element is in the collection.
        /// </summary>
        /// <param name="value">
        /// <para>
        /// Type: <typeparamref name="T"/>
        /// </para>
        /// <para>
        /// The object to locate in the collection. The value can be <see langword="null"/> for reference types.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="System.Boolean"/>
        /// </para>
        /// <para>
        /// <see langword="true"/> if <paramref name="value"/> is found in the collection, <see langword="false"/> otherwise.
        /// </para>
        /// </returns>
        public bool Contains(T value)
        {
            return collection.Contains(value);
        }

        /// <summary>
        /// Copies the entire collection to a compatible one-dimensional array, starting at the specified index of the target array.
        /// </summary>
        /// <param name="array">
        /// <para>
        /// Type: <typeparamref name="T"/>[]
        /// </para>
        /// <para>
        /// The one-dimensional <see cref="System.Array"/> that is the destination of the elements copied from the collection.
        /// The array must have 0-based indexing.
        /// </para>
        /// </param>
        /// <param name="index">
        /// <para>
        /// Type: <see cref="System.Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index in <paramref name="array"/> where the copying begins.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="array"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <paramref name="index"/> is less than 0.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// <paramref name="index"/> is equal to or greater than the length of <paramref name="array"/>.<br/>
        /// -or-<br/>
        /// The number of elements in the collection is greater than the available space from <paramref name="index"/>
        /// to the end of the destination <paramref name="array"/>.
        /// </exception>
        public void CopyTo(T[] array, int index)
        {
            collection.CopyTo(array, index);
        }

        /// <summary>
        /// Searches for an element that matches the conditions defined by the specified predicate, 
        /// and returns the zero-based index of the first occurrence within the entire collection.
        /// </summary>
        /// <param name="predicate">
        /// <para>
        /// Type: <see cref="Func{TSource, TResult}"/> of <typeparamref name="T"/>, <see cref="Boolean"/>
        /// </para>
        /// <para>
        /// The predicate that defines the conditions of the element to search for.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index of the first occurrence of an element that matches the conditions defined by <paramref name="predicate"/>, if found; otherwise, –1.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="predicate"/> is <see langword="null"/>.
        /// </exception>
        public int FindIndex(Func<T, bool> predicate)
        {
            return FindIndex(0, collection.Count, predicate);
        }

        /// <summary>
        /// Searches for an element that matches the conditions defined by the specified predicate, 
        /// and returns the zero-based index of the first occurrence within the range of elements in the collection 
        /// that extends from the specified index to the last element.
        /// </summary>
        /// <param name="startIndex">
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based starting index of the search.
        /// </para>
        /// </param>
        /// <param name="predicate">
        /// <para>
        /// Type: <see cref="Func{TSource, TResult}"/> of <typeparamref name="T"/>, <see cref="Boolean"/>
        /// </para>
        /// <para>
        /// The predicate that defines the conditions of the element to search for.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index of the first occurrence of an element that matches the conditions defined by <paramref name="predicate"/>, if found; otherwise, –1.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="predicate"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <paramref name="startIndex"/> is outside the range of valid indexes for the collection.
        /// </exception>
        public int FindIndex(int startIndex, Func<T, bool> predicate)
        {
            return FindIndex(startIndex, collection.Count - startIndex, predicate);
        }

        /// <summary>
        /// Searches for an element that matches the conditions defined by the specified predicate, 
        /// and returns the zero-based index of the first occurrence within the range of elements in the collection 
        /// that starts at the specified index and contains the specified number of elements
        /// </summary>
        /// <param name="startIndex">
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based starting index of the search.
        /// </para>
        /// </param>
        /// <param name="count">
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The number of elements in the section to search.
        /// </para>
        /// </param>
        /// <param name="predicate">
        /// <para>
        /// Type: <see cref="Func{TSource, TResult}"/> of <typeparamref name="T"/>, <see cref="Boolean"/>
        /// </para>
        /// <para>
        /// The predicate that defines the conditions of the element to search for.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index of the first occurrence of an element that matches the conditions defined by <paramref name="predicate"/>, if found; otherwise, –1.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="predicate"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <paramref name="startIndex"/> is outside the range of valid indexes for the collection.<br/>
        /// -or-<br/>
        /// <paramref name="count"/> is less than 0.<br/>
        /// -or-<br/>
        /// <paramref name="startIndex"/> and <paramref name="count"/> do not specify a valid section in the collection.
        /// </exception>
        public int FindIndex(int startIndex, int count, Func<T, bool> predicate)
        {
            if (predicate == null)
                throw new ArgumentNullException("predicate");
            if (startIndex < 0 || startIndex >= collection.Count)
                throw new ArgumentOutOfRangeException("startIndex");
            if (count < 0)
                throw new ArgumentOutOfRangeException("count");
            if (startIndex + count > collection.Count)
                throw new ArgumentOutOfRangeException();

            for (int i = startIndex; i < count; ++i)
            {
                if (predicate(collection[i]))
                    return i;
            }

            return -1;
        }

        /// <summary>
        /// Searches for the specified object and returns the zero-based index of the first occurrence within the entire collection.
        /// </summary>
        /// <param name="value">
        /// <para>
        /// Type: <typeparamref name="T"/>
        /// </para>
        /// <para>
        /// The object to locate in the collection. The value can be <see langword="null"/> for reference types.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="System.Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index of the first occurence of <paramref name="value"/> within the entire collection,
        /// if found, otherwise -1.
        /// </para>
        /// </returns>
        public int IndexOf(T value)
        {
            return collection.IndexOf(value);
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the element at the specified index.
        /// </summary>
        /// <param name="index">
        /// <para>
        /// Type: <see cref="System.Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index of the element to get or set.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <typeparamref name="T"/>
        /// </para>
        /// <para>
        /// The element at the specified index.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <paramref name="index"/> is less than 0.<br/>
        /// -or-<br/>
        /// <paramref name="index"/> is equal to or greater than <see cref="Count"/>.
        /// </exception>
        public T this[int index]
        {
            get { return collection[index]; }
        }

        /// <summary>
        /// Gets the number of elements actually contained in the wrapped collection.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="System.Int32"/>
        /// </para>
        /// <para>
        /// The number of elements actually contained in the wrapped collection.
        /// </para>
        /// </value>
        public int Count
        {
            get { return collection.Count; }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the wrapped collection.
        /// </summary>
        private IList<T> collection;

        #endregion
    }
}
